/*
   実行環境は
   VMware Fusion で動作する ubuntu 12.04 64bit

   リファレンスは
   http://developer.gnome.org/gtk3/3.4/

   # コンパイルは
   src="my_viewer2"
   opt=$( pkg-config gtk+-3.0 --cflags --libs )
   g++ -o $src $src.cpp $opt

   # 実行は
   img="lenna.png"
   ./$src $img

   os X から ssh -X 使用時 表示をホストに変えるときは
   export DISPLAY=:0.0
   os X に戻すときは
   export DISPLAY=:10.0
*/

#include <iostream>
#include <gtk/gtk.h>

  struct my_data {
    int             argc            ;
    char         ** argv            ;

    GdkPixbuf     * navi_image      ;
    GtkWidget     * navi_canvas     ;
    GtkWidget     * navi_layout     ;
    GtkAdjustment * navi_hadjust    ;
    GtkAdjustment * navi_vadjust    ;
    GtkWidget     * navi_window     ;
    GdkCursorType   navi_cursor     ;

    GtkWidget     * navi_roi        ;
    gboolean        roi_selected    ;
    GdkRectangle    roi             ;

    GdkPixbuf     * expand_image    ;
    GtkWidget     * expand_canvas   ;
    GtkAdjustment * expand_hadjust  ;
    GtkAdjustment * expand_vadjust  ;
    GtkWidget     * expand_window   ;
    GdkCursor     * expand_cursor   ;

    GdkPoint        mouse_posi      ;
    GdkCursor     * mouse_cursor    ;
    GdkEventType    mouse_eventtype ;
  } ;

  #define my_signal_connect( object , signal , callback ) ( \
    g_signal_connect( G_OBJECT( object ), ( signal ), G_CALLBACK( callback ), & data ) \
  )

// ================================================================================
// navi
//   navi_setup , navi_update , navi_roi_update , navi_moveto_roi
//   navi_scrolled , navi_key_pressed
//   on_navi_roi , navi_mouse_pressed , navi_mouse_released , navi_mouse_moved
// --------------------------------------------------------------------------------

  GdkPixbuf * navi_setup(
    gint      navi_width_req ,
    my_data * data
  ) {

    g_print( "navi image ... " ) ;
    std::string file_name ;
      if( data -> argc < 2 ) { g_print( "no file name\n" ) ; return NULL ; }
      file_name = data -> argv[1] ;
    GdkPixbuf * image ;
      image = gdk_pixbuf_new_from_file( file_name .c_str() , NULL );
      if( ! image ) { g_print( "cannot read\n" ); return NULL ; }

    GtkAllocation image_size ;
      image_size .width  = gdk_pixbuf_get_width(  image );
      image_size .height = gdk_pixbuf_get_height( image );
    gdouble navi_k ;
      navi_k = (gdouble) navi_width_req / image_size .width ;
      if( 1.0 < navi_k ) { navi_k = 1.0 ; }
    GtkAllocation navi_size ;
      navi_size .width  = gint( navi_k * image_size .width  + 0.5 );
      navi_size .height = gint( navi_k * image_size .height + 0.5 );

    GdkPixbuf * navi_image ;
      navi_image = gdk_pixbuf_scale_simple(
        image , navi_size .width , navi_size .height , GDK_INTERP_BILINEAR
      );

    g_print( "ready k=%.2f\n", navi_k ) ;
    return navi_image ;
  }

  void navi_update(
    my_data * data
  ) {

    if( data -> navi_image == NULL ) {
      data -> navi_image = navi_setup( 1200 , data );
      if( data -> navi_image   == NULL ) { return ; }
    }

    GtkAllocation layout_size ;
      layout_size .width  = gdk_pixbuf_get_width(  data -> navi_image );
      layout_size .height = gdk_pixbuf_get_height( data -> navi_image );
      gtk_widget_set_size_request( data -> navi_layout , layout_size .width , layout_size .height );

    gtk_image_set_from_pixbuf( GTK_IMAGE( data -> navi_canvas ), data -> navi_image );
  }

  void navi_roi_update(
    my_data * data
  ) {

    gint navi_width ;
      navi_width = gdk_pixbuf_get_width( data -> navi_image );
    gint image_width ;
      image_width = gdk_pixbuf_get_width( data -> expand_image );
    gdouble k ;
      k = (gdouble) navi_width / image_width ;

    GdkPixbuf * roi_image ;
      roi_image = gdk_pixbuf_new(
        GDK_COLORSPACE_RGB , TRUE , 8 , k * data -> roi .width , k * data -> roi .height
      );
      gdk_pixbuf_fill( roi_image , data -> roi_selected ? 0xff000060 : 0x00ff0060 );

    gtk_image_set_from_pixbuf( GTK_IMAGE( data -> navi_roi ), roi_image );
    g_object_unref( roi_image );

    gtk_layout_move(
      GTK_LAYOUT( data -> navi_layout ), data -> navi_roi , k * data -> roi .x , k * data -> roi .y
    );
  }

  void navi_moveto_roi(
    my_data * data
  ) {

    gint navi_width ; gint image_width ;
      navi_width  = gdk_pixbuf_get_width( data -> navi_image );
      image_width = gdk_pixbuf_get_width( data -> expand_image );
    gdouble k ;
      k = (gdouble) navi_width / image_width ;

    gdouble roi_x ; gdouble roi_y ; gdouble roi_width ; gdouble roi_height ;
      roi_x      = k * data -> roi .x ;
      roi_y      = k * data -> roi .y ;
      roi_width  = k * data -> roi .width ;
      roi_height = k * data -> roi .height ;
    gdouble roi_right  ; gdouble roi_bottom ;
      roi_right  = roi_x + roi_width ;
      roi_bottom = roi_y + roi_height ;

    gdouble h_val ; gdouble h_page ; gdouble v_val ; gdouble v_page ;
      h_val  = gtk_adjustment_get_value( data -> navi_hadjust );
      h_page = gtk_adjustment_get_page_size( data -> navi_hadjust );
      v_val  = gtk_adjustment_get_value( data -> navi_vadjust );
      v_page = gtk_adjustment_get_page_size( data -> navi_vadjust );

    gdouble h_new ; gdouble v_new ;
      h_new = ( roi_x     < h_val )           ? roi_x :
              ( roi_right < h_val + h_page )  ? h_val :
              ( h_page    < roi_width )       ? h_val :
              ( h_page    < roi_right )       ? roi_right - h_page :
                                                h_val ;
      v_new = ( roi_y      < v_val )          ? roi_y :
              ( roi_bottom < v_val + v_page ) ? v_val :
              ( v_page     < roi_height )     ? v_val :
              ( v_page     < roi_bottom )     ? roi_bottom - v_page :
                                                v_val ;

    if( h_new != h_val ) { gtk_adjustment_set_value( data -> navi_hadjust , h_new ) ; }
    if( v_new != v_val ) { gtk_adjustment_set_value( data -> navi_vadjust , v_new ) ; }
  }

void navi_scale_clicked (
    GtkWidget * widget ,
    my_data   * data
  ) {

    GtkAllocation image_size ;
      image_size .width  = gdk_pixbuf_get_width(  data -> navi_image );
      image_size .height = gdk_pixbuf_get_height( data -> navi_image );
    gint navi_width ; gint expand_width ;
      navi_width   = gtk_adjustment_get_page_size( data -> navi_hadjust );
      expand_width = gdk_pixbuf_get_width( data -> expand_image );
    gdouble k ;
      k = (gdouble) navi_width / image_size .height ;

    gint width_req ;
      std::string label ;
        label = gtk_button_get_label( GTK_BUTTON( widget ) ) ;
      width_req =
        label .compare( "⇔" ) == 0 ?   navi_width :
        label .compare( "⇕" )  == 0 ?  gint( k * image_size .width ) :
        label .compare( "＋" ) == 0 ?   image_size .width + 150 :
        label .compare( "ー" ) == 0 ?   image_size .width - 150 :
                                        1200 ;
      width_req =
        width_req < 500          ?  500  :
        width_req > 2000         ?  2000 :
        width_req > expand_width ?  expand_width :
                                    width_req ;

    GdkPixbuf * image ; GdkPixbuf * image_new ;
      image = data -> navi_image ;
      image_new = navi_setup( width_req , data );
      if( image_new == NULL ) { return ; }

    data -> navi_image = image_new ;
      g_object_unref( image );
      navi_update( data );
      navi_roi_update( data );
      navi_moveto_roi( data );
  }

  gboolean navi_scrolled (
    GtkWidget      * widget ,
    GdkEventScroll * event  ,
    my_data        * data
  ) {

    if( ! data -> roi_selected ) { return FALSE ; }

    gint k ; gboolean h ;
    switch( event -> direction ) {
      case GDK_SCROLL_UP    :  k = -1 ; h = FALSE ; break;
      case GDK_SCROLL_DOWN  :  k =  1 ; h = FALSE ; break;
      case GDK_SCROLL_LEFT  :  k = -1 ; h = TRUE  ; break;
      case GDK_SCROLL_RIGHT :  k =  1 ; h = TRUE  ; break;
      default : return TRUE ;
    }

    GtkAdjustment * adjust ;
      adjust = h ? data -> expand_hadjust : data -> expand_vadjust ;
    gint val ;
      val = gtk_adjustment_get_value( adjust );
    gint step ;
      step = gtk_adjustment_get_step_increment( adjust );
    gtk_adjustment_set_value( adjust , val + k * 4 * step );

    return TRUE ;
  }

  gboolean navi_tab_pressed (
    GtkWidget   * widget ,
    GdkEventKey * event  ,
    my_data     * data
  ) {
    switch( event -> keyval ) {
      case 0xff09 : // tab
        gtk_window_present( GTK_WINDOW( data -> expand_window ) );
        break ;
      default : return FALSE ;
    }
    return TRUE ;
  }

  gboolean navi_key_pressed (
    GtkWidget   * widget ,
    GdkEventKey * event  ,
    my_data     * data
  ) {

    gint h ; gint v ; gint t ;
      h = 0 ; v = 0 ; t = 0 ;
    switch( event -> keyval ) {
      case 0x0020 : // space
        data -> roi_selected = ! data -> roi_selected ;
        navi_roi_update( data ) ;
        break ;
      case 0xff09 : // tab
        gtk_window_present( GTK_WINDOW( data -> expand_window ) );
        break ;
      case 0xff50 : t = -1 ; break ; // home
      case 0xff57 : t =  1 ; break ; // end
      case 0xff55 : t = -1 ; h = -1 ; break ; // page up
      case 0xff56 : t = -1 ; h =  1 ; break ; // page down
      case 0xff51 : h = -1 ; break ; // ←
      case 0xff52 : v = -1 ; break ; // ↑
      case 0xff53 : h =  1 ; break ; // →
      case 0xff54 : v =  1 ; break ; // ↓
      default : /*
        g_print( "key: %04x - %04x%s%s%s%s\n", event -> keyval ,
          event -> state ,
          event -> state & GDK_SHIFT_MASK        ? " shift"   : "" ,
          event -> state & GDK_CONTROL_MASK      ? " control" : "" ,
          event -> state & GDK_MOD2_MASK         ? " command" : "" ,
          event -> state & GDK_BUTTON5_MASK << 1 ? " option"  : ""
        ) ; // */
        return FALSE ;
    }
    if( event -> state & GDK_SHIFT_MASK        ) { return FALSE ; }
    if( event -> state & GDK_CONTROL_MASK      ) { return FALSE ; }
    if( event -> state & GDK_MOD2_MASK         ) { return FALSE ; }
    if( event -> state & GDK_BUTTON5_MASK << 1 ) { return FALSE ; }

    if( ! data -> roi_selected ) { return FALSE ; }

    gint val ; gint upper ;
      if( h != 0 ) {
        val = gtk_adjustment_get_value( data -> expand_hadjust );
        gtk_adjustment_set_value( data -> expand_hadjust , val + h * data -> roi .width );
      }
      if( v != 0 ) {
        val = gtk_adjustment_get_value( data -> expand_vadjust );
        gtk_adjustment_set_value( data -> expand_vadjust , val + v * data -> roi .height );
      }
      if( t == -1 ) {
        gtk_adjustment_set_value( data -> expand_vadjust , 0 );
      }
      if( t == 1 ) {
        upper = gtk_adjustment_get_upper( data -> expand_vadjust );
        gtk_adjustment_set_value( data -> expand_vadjust , upper - data -> roi .height );
      }

    return TRUE ;
  }

  gboolean on_navi_roi (
    gint            event_x ,
    gint            event_y ,
    GdkCursorType   type ,
    my_data       * data
  ) {
    gint navi_width ; gint image_width ;
      navi_width  = gdk_pixbuf_get_width( data -> navi_image );
      image_width = gdk_pixbuf_get_width( data -> expand_image );
    gdouble k ;
      k = (gdouble) navi_width / image_width ;

    gdouble h ; gdouble v ;
      h = gtk_adjustment_get_value( data -> navi_hadjust ) ;
      v = gtk_adjustment_get_value( data -> navi_vadjust ) ;

    gint x ; gint y ; gint d0 ; gint d1 ;
      x = (gint) ( ( h + event_x ) / k + 0.5 );
      y = (gint) ( ( v + event_y ) / k + 0.5 );
      d0 = 15 ; d1 = 5;

    gboolean result ;
      switch( type ) {
        case GDK_RIGHT_SIDE :
          result = (
            data -> roi .x + data -> roi .width - d0 < x && x < data -> roi .x + data -> roi .width + d1
          );
          break ;
        case GDK_BOTTOM_SIDE :
          result = (
            data -> roi .y + data -> roi .height - d0 < y && y < data -> roi .y + data -> roi .height + d1
          );
          break ;
        case GDK_BOTTOM_RIGHT_CORNER :
          result = (
            data -> roi .x + data -> roi .width  - d0 < x && x < data -> roi .x + data -> roi .width  + d1 &&
            data -> roi .y + data -> roi .height - d0 < y && y < data -> roi .y + data -> roi .height + d1
          );
          break ;
        case GDK_FLEUR :
          result = (
            data -> roi .x < x && x < data -> roi .x + data -> roi .width &&
            data -> roi .y < y && y < data -> roi .y + data -> roi .height
          );
          break ;
        default :
          result = FALSE ;
      }

    return result ;
  }

  gboolean navi_mouse_pressed (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    // save mouse event type
      data -> mouse_eventtype = event -> type ;
    // save mouse position
      data -> mouse_posi .x = event -> x ;
      data -> mouse_posi .y = event -> y ;

    if( event -> type == GDK_BUTTON_PRESS && data -> navi_cursor == GDK_BLANK_CURSOR ) { // first single click
      if( data -> roi_selected && on_navi_roi( event -> x , event -> y , GDK_FLEUR , data ) ) {
        GdkCursorType type ;
          type =
            on_navi_roi( event -> x , event -> y , GDK_BOTTOM_RIGHT_CORNER , data ) ? GDK_BOTTOM_RIGHT_CORNER :
            on_navi_roi( event -> x , event -> y , GDK_RIGHT_SIDE ,  data )         ? GDK_RIGHT_SIDE  :
            on_navi_roi( event -> x , event -> y , GDK_BOTTOM_SIDE , data )         ? GDK_BOTTOM_SIDE :
                                                                                      GDK_FLEUR ;
        data -> mouse_cursor = gdk_window_get_cursor( gtk_widget_get_window( widget ) );
        data -> navi_cursor  = type ;
        gdk_window_set_cursor( gtk_widget_get_window( widget ), gdk_cursor_new( data -> navi_cursor ) );
      }
      else if( ! data -> roi_selected ) {
        data -> mouse_cursor = gdk_window_get_cursor( gtk_widget_get_window( widget ) );
        data -> navi_cursor  = GDK_FLEUR ;
        gdk_window_set_cursor( gtk_widget_get_window( widget ), gdk_cursor_new( data -> navi_cursor ) );
      }
    }

    return FALSE ;
  }

  gboolean navi_mouse_released (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    // restore mouse cursor
      if( data -> navi_cursor != GDK_BLANK_CURSOR ) {
        gdk_window_set_cursor( gtk_widget_get_window( widget ), data -> mouse_cursor );
        data -> mouse_cursor = NULL ;
        data -> navi_cursor = GDK_BLANK_CURSOR ;
      }

    if( data -> mouse_eventtype == GDK_2BUTTON_PRESS ) { // double click
      if( data -> roi_selected || on_navi_roi( event -> x , event -> y , GDK_FLEUR , data ) ) {
        // select roi
          data -> roi_selected = TRUE ;

        // move direct roi
          gint navi_width ; gint image_width ;
            navi_width  = gdk_pixbuf_get_width( data -> navi_image );
            image_width = gdk_pixbuf_get_width( data -> expand_image );
          gdouble k ;
            k = (gdouble) navi_width / image_width ;

          gdouble h0 ; gdouble v0 ;
            h0 = gtk_adjustment_get_value( data -> navi_hadjust ) ;
            v0 = gtk_adjustment_get_value( data -> navi_vadjust ) ;
 
          gtk_adjustment_set_value( data -> expand_hadjust , ( h0 + data -> mouse_posi .x ) / k );
          gtk_adjustment_set_value( data -> expand_vadjust , ( v0 + data -> mouse_posi .y ) / k );

          gdouble h1 ; gdouble v1 ;
            h1 = gtk_adjustment_get_value( data -> navi_hadjust ) ;
            v1 = gtk_adjustment_get_value( data -> navi_vadjust ) ;

        // warp mouse pointer
          gint x0 ; gint y0 ; gint x1 ; gint y1 ;
            x0 = data -> mouse_posi .x ;
            y0 = data -> mouse_posi .y ;
            x1 = k * data -> roi .x - h1 ;
            y1 = k * data -> roi .y - v1 ;

          if( x0 != x1 || y0 != y1 ) {
            GdkDisplay * display ;  GdkDeviceManager * dev_manager ; GdkDevice * device ;
              display = gdk_window_get_display( gtk_widget_get_window( widget ) );
              dev_manager = gdk_display_get_device_manager( display );
              device = gdk_device_manager_get_client_pointer( dev_manager );
            GdkScreen * screen ; gint x2 ; gint y2 ;
              gdk_device_get_position( device , & screen , & x2 , & y2 );
              gdk_device_warp( device , screen , x1 + x2 - x0 , y1 + y2 - y0 );
          }

      }
      return FALSE ;
    }

    gdouble move_x , move_y ;
      move_x = event -> x - data -> mouse_posi .x ;
      move_y = event -> y - data -> mouse_posi .y ;
    gdouble d ;
      d = 1.0 ;

    if( data -> roi_selected && on_navi_roi( event -> x , event -> y , GDK_FLEUR , data ) &&
        ( - d < move_x && move_x < d && - d < move_y && move_y < d ) ) {
      // deselect roi
        data -> roi_selected = FALSE ;
        navi_roi_update( data );
    }
    else if( ! data -> roi_selected && on_navi_roi( event -> x , event -> y , GDK_FLEUR , data ) ) {
      // select roi
        data -> roi_selected = TRUE ;
        navi_roi_update( data );
    }

    return FALSE ;
  }

  gboolean navi_mouse_moved (
    GtkWidget      * widget ,
    GdkEventMotion * event  ,
    my_data        * data
  ) {

    if( data -> navi_cursor == GDK_BLANK_CURSOR ) { return FALSE ; }

    gdouble move_x ; gdouble move_y ; gdouble d ;
      move_x = event -> x - data -> mouse_posi .x ;
      move_y = event -> y - data -> mouse_posi .y ;
      d = 2.5 ;

    if( - d < move_x && move_x < d && - d < move_y && move_y < d ) {} else { // mouse moved

        gint image_width ; gint image_height ;
          image_width  = gdk_pixbuf_get_width(  data -> expand_image );
          image_height = gdk_pixbuf_get_height( data -> expand_image );
        gint navi_width ;
          navi_width  = gdk_pixbuf_get_width( data -> navi_image );
        gdouble k ;
          k = (gdouble) navi_width / image_width ;

        if( data -> navi_cursor == GDK_FLEUR && data -> roi_selected ) { // scroll expand
          gint h ; gint v ;
            h = gtk_adjustment_get_value( data -> expand_hadjust );
            v = gtk_adjustment_get_value( data -> expand_vadjust );
          gtk_adjustment_set_value( data -> expand_hadjust , h + move_x / k );
          gtk_adjustment_set_value( data -> expand_vadjust , v + move_y / k );
        }
        else if( data -> navi_cursor == GDK_FLEUR && ! data -> roi_selected ) { // scroll navi
          gdouble h ; gdouble v ;
            h = gtk_adjustment_get_value( data -> navi_hadjust ) ;
            v = gtk_adjustment_get_value( data -> navi_vadjust ) ;
          gtk_adjustment_set_value( data -> navi_hadjust , h - move_x );
          gtk_adjustment_set_value( data -> navi_vadjust , v - move_y );
        }
        else { // resize expand
          if( data -> navi_cursor == GDK_RIGHT_SIDE || data -> navi_cursor == GDK_BOTTOM_RIGHT_CORNER ) {
            gint mv_x ;
              mv_x = gint( move_x / k + 0.5 );
            data -> roi .width  = ( data -> roi .x + data -> roi .width + mv_x < image_width ) ?
                                    data -> roi .width + mv_x : image_width - data -> roi .x ;
          }
          if( data -> navi_cursor == GDK_BOTTOM_SIDE || data -> navi_cursor == GDK_BOTTOM_RIGHT_CORNER ) {
            gint mv_y ;
              mv_y = gint( move_y / k + 0.5 );
            data -> roi .height = ( data -> roi .y + data -> roi .height + mv_y < image_height ) ?
                                    data -> roi .height + mv_y : image_height - data -> roi .y ;
          }
          gtk_window_resize (
            GTK_WINDOW( data -> expand_window ), data -> roi .width , data -> roi .height
          );
          gtk_widget_queue_draw( data -> expand_window );
        }

      // save mouse event
        data -> mouse_posi .x = event -> x ;
        data -> mouse_posi .y = event -> y ;
    }

    return FALSE ;
  }

// ================================================================================
// expand
// --------------------------------------------------------------------------------

  GdkPixbuf * expand_setup(
    my_data * data
  ) {

    g_print( "expand image ... " ) ;
    std::string file_name ;
      if( data -> argc < 2 ) { g_print( "no file name\n" ) ; return NULL ; }
      file_name = data -> argv[1] ;
    GdkPixbuf * image ;
      image = gdk_pixbuf_new_from_file( file_name .c_str() , NULL );
      if( ! image ) { g_print( "cannot read\n" ); return NULL ; }
    g_print( "ready\n" ) ;

    return image ;
  }

  void expand_update(
    my_data * data
  ) {

    if( data -> expand_image == NULL ) {
      data -> expand_image = expand_setup( data );
      if( data -> expand_image   == NULL ) { return ; }

      // update expand size
        GtkAllocation image_size ;
          image_size .width  = gdk_pixbuf_get_width(  data -> expand_image );
          image_size .height = gdk_pixbuf_get_height( data -> expand_image );
        // roi
          data -> roi .width  = gint( (gdouble) image_size .width  / 4 + 0.5 );
          data -> roi .height = gint( (gdouble) image_size .height / 5 + 0.5 );
        // expand window
          gtk_window_set_default_size(
            GTK_WINDOW( data -> expand_window ), data -> roi .width , data -> roi .height
          );
          gtk_adjustment_set_upper( data -> expand_hadjust , image_size .width );
          gtk_adjustment_set_upper( data -> expand_vadjust , image_size .height );
    }

    GdkPixbuf * sub_image ;
      sub_image = gdk_pixbuf_new_subpixbuf(
        data -> expand_image ,
        data -> roi .x , data -> roi .y , data -> roi .width , data -> roi .height
      );

    gtk_image_set_from_pixbuf( GTK_IMAGE( data -> expand_canvas ), sub_image );
    g_object_unref( sub_image );
  }

  gboolean expand_h_changed(
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint val ; gint upper ;
      val   = gtk_adjustment_get_value( adjustment );
      upper = gtk_adjustment_get_upper( adjustment );
    gint x_limit ;
      x_limit = upper - data -> roi .width ;

    data -> roi .x = ( val < x_limit ) ? val : x_limit ;
    expand_update( data );
    navi_roi_update( data );
    navi_moveto_roi( data );
    return FALSE ;
  }

  gboolean expand_v_changed(
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint val ; gint upper ;
      val   = gtk_adjustment_get_value( adjustment );
      upper = gtk_adjustment_get_upper( adjustment );
    gint y_limit ;
      y_limit = upper - data -> roi .height ;

    data -> roi .y = ( val < y_limit ) ? val : y_limit ;
    expand_update( data );
    navi_roi_update( data );
    navi_moveto_roi( data );
    return FALSE ;
  }

  gboolean expand_width_changed (
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint page ;
      page = gtk_adjustment_get_page_size( adjustment );
    gint width_limit ;
      width_limit = gtk_adjustment_get_upper( data -> expand_hadjust );

    if( data -> roi .x + page > width_limit ) {
      data -> roi .x = ( page < width_limit ) ? width_limit - page : 0 ;
      gtk_adjustment_set_value( data -> expand_hadjust , data -> roi .x );
    }
    data -> roi .width = ( page < width_limit ) ? page : width_limit ;
      gtk_adjustment_set_page_size( data -> expand_hadjust , data -> roi .width );
      gtk_adjustment_set_page_increment( data -> expand_hadjust , data -> roi .width );

    gtk_adjustment_set_upper( adjustment , page );

    expand_update( data );
    navi_roi_update( data );
    navi_moveto_roi( data );
    return FALSE ;
  }

  gboolean expand_height_changed (
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint page ;
      page = gtk_adjustment_get_page_size( adjustment );
    gint height_limit ;
      height_limit = gtk_adjustment_get_upper( data -> expand_vadjust );

    if( data -> roi .y + page > height_limit ) {
      data -> roi .y = ( page < height_limit ) ? height_limit - page : 0 ;
      gtk_adjustment_set_value( data -> expand_vadjust , data -> roi .y );
    }
    data -> roi .height = ( page < height_limit ) ? page : height_limit ;
      gtk_adjustment_set_page_size( data -> expand_vadjust , data -> roi .height );
      gtk_adjustment_set_page_increment( data -> expand_vadjust , data -> roi .height );

    gtk_adjustment_set_upper( adjustment , page );

    expand_update( data );
    navi_roi_update( data );
    navi_moveto_roi( data );
    return FALSE ;
  }

  gboolean expand_scrolled (
    GtkWidget      * widget ,
    GdkEventScroll * event  ,
    my_data        * data
  ) {

    gint k ; gboolean h ;
    switch( event -> direction ) {
      case GDK_SCROLL_UP    :  k = -1 ; h = FALSE ; break;
      case GDK_SCROLL_DOWN  :  k =  1 ; h = FALSE ; break;
      case GDK_SCROLL_LEFT  :  k = -1 ; h = TRUE  ; break;
      case GDK_SCROLL_RIGHT :  k =  1 ; h = TRUE  ; break;
      default : return FALSE ;
    }

    GtkAdjustment * adjust ;
      adjust = h ? data -> expand_hadjust : data -> expand_vadjust ;
    gint val ;
      val = gtk_adjustment_get_value( adjust );
    gint step ;
      step = gtk_adjustment_get_step_increment( adjust );
    gtk_adjustment_set_value( adjust , val + k * 4 * step );

    return FALSE ;
  }

  gboolean expand_key_pressed (
    GtkWidget   * widget ,
    GdkEventKey * event  ,
    my_data     * data
  ) {

    gint p ; gint q ; gint r ; gint s ; gboolean h ;
        p = 0 ; q = 0 ; r = 1 ; s = 0 ; h = FALSE ;
    switch( event -> keyval ) {
      case 0xff09 : // tab
        gtk_window_present( GTK_WINDOW( data -> navi_window ) );
        break ;
      case 0xff50 : r =  0 ; break ; // home
      case 0xff57 : q =  1 ; break ; // end
      case 0xff55 : p = -1 ; break ; // page up
      case 0xff56 : p =  1 ; break ; // page down
      case 0xff51 : s = -1 ; h = TRUE ; break ; // ←
      case 0xff52 : s = -1 ;            break ; // ↑
      case 0xff53 : s =  1 ; h = TRUE ; break ; // →
      case 0xff54 : s =  1 ;            break ; // ↓
      default : /*
        g_print( "key: %04x - %04x%s%s%s%s\n", event -> keyval ,
          event -> state ,
          event -> state & GDK_SHIFT_MASK        ? " shift"   : "" ,
          event -> state & GDK_CONTROL_MASK      ? " control" : "" ,
          event -> state & GDK_MOD2_MASK         ? " command" : "" ,
          event -> state & GDK_BUTTON5_MASK << 1 ? " option"  : ""
        ) ; // */
        return FALSE ;
    }
    if( event -> state & GDK_SHIFT_MASK ) { return FALSE ; }

    GtkAdjustment * adjust ;
      adjust = ( s == 0 && event -> state & GDK_CONTROL_MASK || h ) ?
                 data -> expand_hadjust : data -> expand_vadjust ;
    gint val ; gint step ; gint page ; gint upper ;
      val   = gtk_adjustment_get_value( adjust );
      step  = gtk_adjustment_get_step_increment( adjust );
      page  = gtk_adjustment_get_page_increment( adjust );
      upper = gtk_adjustment_get_upper( adjust );
    gint d ;
      d = ( event -> state & GDK_CONTROL_MASK ) ? 9 * step : 1 ;

    gint val_new ;
      val_new = p * page + q * upper + r * val + s * d ;
      val_new = ( val_new < 0 ) ? 0 : ( val_new > upper ) ? upper : val_new ;
    gtk_adjustment_set_value( adjust , val_new );

    return FALSE ;
  }

  gboolean expand_mouse_pressed (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    if( data -> expand_cursor != NULL ) { return FALSE ; }

    // change cursor
      data -> mouse_cursor = gdk_window_get_cursor( gtk_widget_get_window( widget ) );
      data -> expand_cursor   = gdk_cursor_new( GDK_FLEUR );
      gdk_window_set_cursor( gtk_widget_get_window( widget ), data -> expand_cursor );

    // save mouse position
      data -> mouse_posi .x = event -> x ;
      data -> mouse_posi .y = event -> y ;

    return FALSE ;
  }

  gboolean expand_mouse_released (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    if( data -> expand_cursor == NULL ) { return FALSE ; }

    // restore mouse cursor
      if( data -> expand_cursor != NULL ) {
        gdk_window_set_cursor( gtk_widget_get_window( widget ), data -> mouse_cursor );
        data -> mouse_cursor = NULL ;
        data -> expand_cursor = NULL ;
      }

    return FALSE ;
  }

  gboolean expand_mouse_moved (
    GtkWidget      * widget ,
    GdkEventMotion * event  ,
    my_data        * data
  ) {

    if( data -> expand_cursor == NULL ) { return FALSE ; }

    GdkPoint move ;
      move .x = event -> x - data -> mouse_posi .x ;
      move .y = event -> y - data -> mouse_posi .y ;

    // move step
      gint d ;
        d = 4 ;
      if( - d < move .x && move .x < d && - d < move .y && move .y < d ) { return FALSE ; }

    // scroll expand
      gint h ; gint v ;
        h = gtk_adjustment_get_value( data -> expand_hadjust );
        v = gtk_adjustment_get_value( data -> expand_vadjust );
      gtk_adjustment_set_value( data -> expand_hadjust , h - move .x );
      gtk_adjustment_set_value( data -> expand_vadjust , v - move .y );

    // save mouse position
      data -> mouse_posi .x = event -> x ;
      data -> mouse_posi .y = event -> y ;

    return FALSE ;
  }

// ================================================================================
// main
// --------------------------------------------------------------------------------

int main(
  int     argc ,
  char ** argv
) {

  gtk_init( & argc , & argv );
    g_print( "--- %s message ---\n", argv[0] ) ;

  struct my_data data ; {
    data .argc = argc ;
    data .argv = argv ;

    data .navi_image   = NULL ;
    data .expand_image = NULL ;

    data .navi_cursor  = GDK_BLANK_CURSOR ;

    data .roi .x = 0 ;
    data .roi .y = 0 ;
    data .roi .width  = 300 ;
    data .roi .height = 340 ;
    data .roi_selected = TRUE ;

    data .expand_cursor = NULL ;

    data .mouse_posi .x = 0 ;
    data .mouse_posi .y = 0 ;
    data .mouse_cursor = NULL ;
    data .mouse_eventtype = GDK_NOTHING ;
  }

  GtkWidget * navi_window ; {

    navi_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
      GtkAllocation default_size ;
        default_size .width  = 800 ;
        default_size .height = 700 ;
      gdouble k ;
        k = 0.6 ;
      GtkAllocation request_size ;
        request_size .width  = (gint)( k * default_size .width  + 0.5 );
        request_size .height = (gint)( k * default_size .height + 0.5 );
      gtk_widget_set_size_request( navi_window , request_size .width , request_size .height );
      gtk_window_set_default_size( GTK_WINDOW( navi_window ), default_size .width , default_size .height );
      gtk_window_move( GTK_WINDOW( navi_window ), 0 , 0 );
      gtk_widget_add_events( navi_window , GDK_POINTER_MOTION_HINT_MASK );
      my_signal_connect( navi_window , "key-press-event" , navi_tab_pressed );
      my_signal_connect( navi_window , "button-press-event" , navi_mouse_pressed );
      my_signal_connect( navi_window , "button-release-event" , navi_mouse_released );
      my_signal_connect( navi_window , "motion-notify-event" , navi_mouse_moved );
      g_signal_connect( G_OBJECT( navi_window ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );
    data .navi_window = navi_window ;

    GtkWidget * navi_vbox ;
      navi_vbox = gtk_box_new( GTK_ORIENTATION_VERTICAL , 0 );
      gtk_container_add( GTK_CONTAINER( navi_window ) , navi_vbox );

    data .navi_canvas = gtk_image_new_from_pixbuf( NULL );

    data .navi_roi = gtk_image_new_from_pixbuf( NULL );

    data .navi_layout ;
      GtkAllocation layout_size ;
        layout_size .width  = 1200 ;
        layout_size .height = 1700 ;
      data .navi_layout = gtk_layout_new( NULL , NULL );
      gtk_widget_set_size_request( data .navi_layout , layout_size .width , layout_size .height );
      gtk_layout_put( GTK_LAYOUT( data .navi_layout ), data .navi_canvas , 0 , 0 );
      gtk_layout_put( GTK_LAYOUT( data .navi_layout ), data .navi_roi , 0 , 0 );

    GtkWidget * navi_pane;
    navi_pane = gtk_scrolled_window_new( NULL , NULL );
      gtk_scrolled_window_set_policy(
        GTK_SCROLLED_WINDOW( navi_pane ), GTK_POLICY_ALWAYS , GTK_POLICY_ALWAYS
      );
      gtk_scrolled_window_add_with_viewport( GTK_SCROLLED_WINDOW( navi_pane ) , data .navi_layout );
      my_signal_connect( navi_pane , "key-press-event" , navi_key_pressed );
      my_signal_connect( navi_pane , "scroll-event" , navi_scrolled );
      gtk_box_pack_end( GTK_BOX( navi_vbox ) , navi_pane , TRUE, TRUE, 0 );

    data .navi_hadjust = gtk_scrolled_window_get_hadjustment( GTK_SCROLLED_WINDOW( navi_pane ) );
    data .navi_vadjust = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW( navi_pane ) );

    GtkWidget* navi_scale ; // scale button
      navi_scale = gtk_box_new( GTK_ORIENTATION_HORIZONTAL , 0 );
      gtk_box_pack_end( GTK_BOX( navi_vbox ), navi_scale, FALSE, FALSE, 0 );
      gtk_box_reorder_child( GTK_BOX( navi_vbox ), navi_scale , 0 );

    GtkWidget* scale_button[5] ; // buttons
      #define navi_scale_add( col , label ) ( \
        scale_button[ col ] = gtk_button_new_with_label( label ), \
        gtk_box_pack_start( GTK_BOX( navi_scale ), scale_button[ col ], FALSE, FALSE, 0 ), \
        my_signal_connect( scale_button[ col ], "clicked", navi_scale_clicked ) \
      )
      navi_scale_add( 0, "＋" ) ;
      navi_scale_add( 1, "ー" ) ;
      navi_scale_add( 2, "⇕" ) ;
      navi_scale_add( 3, "⇔" ) ;
  }

  GtkWidget * expand_window ; {
    expand_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
      gtk_window_set_title( GTK_WINDOW( expand_window ), "expand" );
      gtk_window_set_default_size( GTK_WINDOW( expand_window ), data .roi .width , data .roi .height );
      gtk_window_set_position( GTK_WINDOW( expand_window ), GTK_WIN_POS_CENTER );
      // gtk_window_iconify( GTK_WINDOW( expand_window ) );
      gtk_widget_add_events( expand_window , GDK_POINTER_MOTION_HINT_MASK );
      my_signal_connect( expand_window , "key-press-event" , expand_key_pressed );
      my_signal_connect( expand_window , "button-press-event" , expand_mouse_pressed );
      my_signal_connect( expand_window , "button-release-event" , expand_mouse_released );
      my_signal_connect( expand_window , "motion-notify-event" , expand_mouse_moved );
      g_signal_connect( G_OBJECT( expand_window ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );
    data .expand_window = expand_window ;

    GtkWidget * expand_vbox ;
      expand_vbox = gtk_box_new( GTK_ORIENTATION_VERTICAL , 0 );
      gtk_container_add( GTK_CONTAINER( expand_window ) , expand_vbox );

    GtkWidget * expand_hbox ;
      expand_hbox = gtk_box_new( GTK_ORIENTATION_HORIZONTAL , 0 );
      gtk_box_pack_start( GTK_BOX( expand_vbox ) , expand_hbox , TRUE , TRUE , 0 );

    data .expand_canvas = gtk_image_new_from_pixbuf( NULL );

    GtkWidget * expand_pane;
      expand_pane = gtk_scrolled_window_new( NULL , NULL );
      gtk_scrolled_window_set_policy(
        GTK_SCROLLED_WINDOW( expand_pane ), GTK_POLICY_ALWAYS , GTK_POLICY_ALWAYS
      );
      gtk_scrolled_window_set_placement(
        GTK_SCROLLED_WINDOW( expand_pane ) , GTK_CORNER_BOTTOM_RIGHT
      );
      gtk_scrolled_window_add_with_viewport(
        GTK_SCROLLED_WINDOW( expand_pane ) , data .expand_canvas
      );
      my_signal_connect( expand_pane , "scroll-event" , expand_scrolled );
      gtk_box_pack_start( GTK_BOX( expand_hbox ) , expand_pane , TRUE , TRUE , 0 );

    GtkAdjustment * hadjust ;
      hadjust = gtk_scrolled_window_get_hadjustment( GTK_SCROLLED_WINDOW( expand_pane ) );
      my_signal_connect( hadjust , "changed" , expand_width_changed );

    GtkAdjustment * vadjust ;
      vadjust = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW( expand_pane ) );
      my_signal_connect( vadjust , "changed" , expand_height_changed );

    GtkWidget * expand_hscrollbar ; {
      gint page ;
        page = data .roi .width ;
      data .expand_hadjust = gtk_adjustment_new( 0 , 0 , page , 10 , page , page );
        my_signal_connect( data .expand_hadjust , "value-changed" , expand_h_changed );
      expand_hscrollbar = gtk_scrollbar_new( GTK_ORIENTATION_HORIZONTAL , data .expand_hadjust );
        gtk_box_pack_start( GTK_BOX( expand_vbox ) , expand_hscrollbar , FALSE , FALSE , 0 );
    }

    GtkWidget * expand_vscrollbar ; {
      gint page ;
        page = data .roi .height ;
      data .expand_vadjust = gtk_adjustment_new( 0 , 0 , page , 10 , page , page );
        my_signal_connect( data .expand_vadjust , "value-changed" , expand_v_changed );
      expand_vscrollbar = gtk_scrollbar_new( GTK_ORIENTATION_VERTICAL , data .expand_vadjust );
        gtk_box_pack_start( GTK_BOX( expand_hbox ) , expand_vscrollbar , FALSE , FALSE , 0 );
    }
  }

  navi_update( & data );
    if( data .navi_image   == NULL ) { return -1 ; }
  expand_update( & data );
    if( data .expand_image == NULL ) { return -1 ; }
  navi_roi_update( & data );

  gtk_widget_show_all( expand_window );
  gtk_widget_show_all( navi_window );
  gtk_main( );

  g_object_unref( data .navi_image );
  g_object_unref( data .expand_image );
  return 0 ;
}

